<?php

/**
 * JingYao-backend
 *
 * @link     https://gitee.com/wang-zhihui-release/jingyao-backend
 * @apiDocument https://gitee.com/wang-zhihui-release/jingyao-backend/wikis/
 */

namespace App\Services;

use App\Exceptions\ApiException;
use App\Format\Code2SessionResultFormat;
use App\Format\WxUserInfoDecryptDataFormat;
use App\Format\WxUserPhoneFormat;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

class XcxService
{
    const CODE_2_SESSION_URL = 'https://api.weixin.qq.com/sns/jscode2session';

    const REDIS_SESSION_KEY_PRE = 'wx_session_key_%s';

    protected $httpService;

    protected $userService;

    private $appId;

    private $secret;

    public function __construct(HttpService $httpService, UserService $userService)
    {
        $this->httpService = $httpService;
        $this->userService = $userService;
        $this->appId = config('jingcai_xcx.app_id');
        $this->secret = config('jingcai_xcx.secret');
    }

    public function fetchCode2Session(string $jsCode)
    {
        try {
            $resultJson = $this->httpService->get(self::CODE_2_SESSION_URL, [
                'appid' => $this->appId,
                'secret' => $this->secret,
                'js_code' => $jsCode,
                'grant_type' => 'authorization_code',
            ]);
        } catch (\Exception $exception) {
            throw new ApiException($exception->getMessage(), 1);
        }
        $result = json_decode($resultJson, true);
        $resultFormat = new Code2SessionResultFormat($result);
        if ($resultFormat->getErrcode() != 0) {
            throw new ApiException($resultFormat->getErrmsg(), 1);
        }

        /** @var null|User $user */
        $user = User::query()->where('wx_openid', $resultFormat->getOpenid())->first();
        // 用户存在 直接登陆
        if (! empty($user) && strlen($user->phone) > 10) {
            return $this->userService->saveLoginToken($user->id);
        }
        // 用户不存在 要小程序端调起 userProfile 继续 decryptUserInfoData 和 decryptUserPhoneData
        // 把微信的 session_key 缓存一下以备用
        // 但是需要一个唯一标记
        $clientId = $this->genClientId();
        $key = sprintf(self::REDIS_SESSION_KEY_PRE, $clientId);
        $cacheData = json_encode([
            'session_key' => $resultFormat->getSessionKey(),
            'openid' => $resultFormat->getOpenid(),
        ]);
        Redis::connection()->set($key, $cacheData);
        Redis::expire($key, 3600);
        if (empty($user)) {
            return [
                'data' => [
                    'client_id' => $clientId,
                ],
                'error_code' => config('error_code.pre_login'),
            ];
        }
        // 用户存在但是没有手机号 要小程序调起获取用户手机号的请求
        if (! empty($user) && strlen($user->phone) < 10) {
            return [
                'data' => [
                    'user_id' => $user->id,
                    'client_id' => $clientId,
                ],
                'error_code' => config('error_code.pre_login_get_phone'),
            ];
        }
    }

    public function decryptUserInfoData(string $encryptedData, string $iv, string $clientId): WxUserInfoDecryptDataFormat
    {
        $data = $this->decryptData($encryptedData, $iv, $clientId);
        Log::debug(__METHOD__ . ' 解析用户信息 ', [
            'wx_user_info' => $data,
        ]);
        $key = sprintf(self::REDIS_SESSION_KEY_PRE, $clientId);
        $cached = Redis::connection()->get($key);
        $cacheData = json_decode($cached, true);
        $openid = $cacheData['openid'];
        $format = new WxUserInfoDecryptDataFormat($data);
        $format->setOpenid($openid);
        Log::debug(__METHOD__ . ' 解析用户信息 ', [
            'wx_user_info' => $format->toArray(),
        ]);
        return $format;
    }

    public function decryptUserPhoneData(string $encryptedData, string $iv, string $clientId)
    {
        $data = $this->decryptData($encryptedData, $iv, $clientId);
        Log::info('解析手机号结果', $data);
        $format = new WxUserPhoneFormat($data);
        $key = sprintf(self::REDIS_SESSION_KEY_PRE, $clientId);
        Redis::connection()->del($key);
        // 如果获取失败 要小程序进入绑定手机号页面
        if (strlen($format->getPhoneNumber()) < 10) {
            Log::error('解析手机号失败', [
                'encryptedData' => $encryptedData,
                'iv' => $iv,
                'client_id' => $clientId,
            ]);
            throw new ApiException('解析手机号失败', 1);
        }
        return $format;
    }

    private function decryptData(string $encryptedData, string $iv, string $clientId): array
    {
        if (strlen($iv) != 24) {
            throw new ApiException('加密算法的初始向量错误', 1);
        }

        $key = sprintf(self::REDIS_SESSION_KEY_PRE, $clientId);
        $cached = Redis::connection()->get($key);
        $cacheData = json_decode($cached, true);
        $sessionKey = $cacheData['session_key'];
        if (strlen($sessionKey) != 24) {
            throw new ApiException('session_key 获取异常', 1);
        }

        $aesKey = base64_decode($sessionKey);

        $aesIV = base64_decode($iv);

        $aesCipher = base64_decode($encryptedData);

        $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);

        $data = json_decode($result, true);

        if ($data == false) {
            Log::error('session_key 解析 encryptedData 结果为', ['data' => $result]);
            throw new ApiException('session_key 解析失败', 1);
        }
        if ($data['watermark']['appid'] != $this->appId) {
            Log::error('appid error ---- ' . $data['watermark']['appid']);
            throw new ApiException('appid 错误', 1);
        }
        return $data;
    }

    private function genClientId()
    {
        return Str::random();
    }
}
